package com.ryan.mdp.service;

import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;

import javax.jms.DeliveryMode;
import javax.jms.JMSException;
import javax.jms.Message;
import javax.jms.MessageFormatException;
import javax.jms.MessageProducer;
import javax.jms.Session;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.jms.listener.SessionAwareMessageListener;
import org.springframework.jms.support.JmsUtils;
import org.springframework.jms.support.converter.MessageConverter;
import org.springframework.jms.support.converter.SimpleMessageConverter;
import org.springframework.remoting.support.RemoteInvocation;
import org.springframework.remoting.support.RemoteInvocationBasedExporter;
import org.springframework.remoting.support.RemoteInvocationResult;
import org.springframework.remoting.support.RemoteInvocationTraceInterceptor;
import org.springframework.util.ClassUtils;

import com.ryan.mdp.annotation.parser.MdpMethodConfig;
import com.ryan.mdp.annotation.parser.MdpMethodParser;
import com.ryan.mdp.annotation.parser.MdpServiceConfig;
import com.ryan.mdp.callback.MdpCallback;
import com.ryan.mdp.callback.MdpCallbackInterceptor;
import com.ryan.mdp.exception.MdpMessages;
import com.ryan.mdp.json.MdpInvocationResult;

public class MdpInvokerServiceExporter extends RemoteInvocationBasedExporter implements SessionAwareMessageListener,
        InitializingBean {
    private final Logger logger = LoggerFactory.getLogger(getClass());
    private final static long DEFAULT_RESPONSE_TTL = 10 * 60 * 1000;
    private String serverName;
    private MessageConverter messageConverter = new SimpleMessageConverter();
    private boolean ignoreInvalidRequests = true;
    private Object proxy;
    private MdpServiceConfig mdpServiceConfig;
    private final Map<String, Method> methodCache = new HashMap<String, Method>();

    public void onMessage(Message requestMessage, Session session) throws JMSException {
        MdpContext context = new MdpContext(getServerName(), requestMessage);
        MdpContextHolder.setCurrentContext(context);
        try {
            RemoteInvocation invocation = readRemoteInvocation(requestMessage);
            if (invocation != null) {
                MdpMethodConfig mdpMethodConfig = getMethodConfig(invocation);
                if (mdpMethodConfig != null) {// 同步有返回值
                    logger.info("invoking mdp service [{}]", mdpMethodConfig.getSource());
                    if (mdpMethodConfig.isSync()) {
                        RemoteInvocationResult result = invokeAndCreateResult(invocation, this.proxy);
                        MdpInvocationResult mdpResult = null;
                        if (result != null) {
                            mdpResult = new MdpInvocationResult(result.getValue(), result.getException());
                        }
                        writeRemoteInvocationResult(requestMessage, session, invocation, mdpResult);
                    } else if (mdpMethodConfig.isCallback()) {
                        proxyMdpCallback(requestMessage, session, invocation);
                        invokeAndCreateResult(invocation, this.proxy);
                    } else {
                        invokeAndCreateResult(invocation, this.proxy);
                    }
                } else {// invalid message
                    onInvalidRequest(requestMessage);
                }
            } else {// invalid message
                onInvalidRequest(requestMessage);
            }
        } finally {
            MdpContextHolder.resetContext();
        }
    }

    public void setMessageConverter(MessageConverter messageConverter) {
        this.messageConverter = (messageConverter != null ? messageConverter : new SimpleMessageConverter());
    }

    /**
     * Set whether invalidly formatted messages should be discarded. Default is
     * "true".
     * <p>
     * Switch this flag to "false" to throw an exception back to the listener
     * container. This will typically lead to redelivery of the message, which
     * is usually undesirable - since the message content will be the same (that
     * is, still invalid).
     */
    public void setIgnoreInvalidRequests(boolean ignoreInvalidRequests) {
        this.ignoreInvalidRequests = ignoreInvalidRequests;
    }

    public void afterPropertiesSet() {
        mdpServiceConfig = MdpMethodParser.parseMdpInterface(getServiceInterface());
        this.proxy = getProxyForService();
    }

    /**
     * Read a RemoteInvocation from the given JMS message.
     * 
     * @param requestMessage
     *            current request message
     * @return the RemoteInvocation object (or <code>null</code> in case of an
     *         invalid message that will simply be ignored)
     * @throws javax.jms.JMSException
     *             in case of message access failure
     */
    protected RemoteInvocation readRemoteInvocation(Message requestMessage) throws JMSException {
        Object content = null;
        try {
            content = this.messageConverter.fromMessage(requestMessage);
        } catch (Throwable e) {
            throw new JMSException(e.getMessage());
        }
        if (content instanceof RemoteInvocation) {
            return (RemoteInvocation) content;
        }
        return onInvalidRequest(requestMessage);
    }

    /**
     * Send the given RemoteInvocationResult as a JMS message to the originator.
     * 
     * @param requestMessage
     *            current request message
     * @param session
     *            the JMS Session to use
     * @param result
     *            the RemoteInvocationResult object
     * @throws javax.jms.JMSException
     *             if thrown by trying to send the message
     */
    public void writeRemoteInvocationResult(Message requestMessage, Session session, RemoteInvocation invocation,
            MdpInvocationResult result) throws JMSException {
        Message response = createResponseMessage(requestMessage, session, result);
        MessageProducer producer = session.createProducer(requestMessage.getJMSReplyTo());
        MdpMethodConfig mConfig = getMethodConfig(invocation);
        try {
            producer.send(response, DeliveryMode.NON_PERSISTENT, mConfig.getPriority(),
                    mConfig.getTtl() > 0 ? mConfig.getTtl() : DEFAULT_RESPONSE_TTL);
            logger.info("sending response for {}", mConfig.getSource());
        } finally {
            JmsUtils.closeMessageProducer(producer);
        }
    }

    /**
     * Create the invocation result response message.
     * <p>
     * The default implementation creates a JMS ObjectMessage for the given
     * RemoteInvocationResult object. It sets the response's correlation id to
     * the request message's correlation id, if any; otherwise to the request
     * message id.
     * 
     * @param request
     *            the original request message
     * @param session
     *            the JMS session to use
     * @param result
     *            the invocation result
     * @return the message response to send
     * @throws javax.jms.JMSException
     *             if creating the messsage failed
     */
    protected Message createResponseMessage(Message request, Session session, MdpInvocationResult result)
            throws JMSException {
        Message response = this.messageConverter.toMessage(result, session);
        String correlation = request.getJMSCorrelationID();
        if (correlation != null) {
            response.setJMSCorrelationID(correlation);
        }
        return response;
    }

    /**
     * 替换异步中MdpCallback实现
     * 
     * @param invocation
     */
    private void proxyMdpCallback(Message requestMessage, Session session, RemoteInvocation invocation) {
        Object[] args = invocation.getArguments();
        Class<?>[] paramTypes = invocation.getParameterTypes();
        for (int i = 0; i < args.length; i++) {
            if (paramTypes[i] == MdpCallback.class) {
                args[i] = ProxyFactory.getProxy(MdpCallback.class, new MdpCallbackInterceptor(requestMessage, session,
                        invocation, this));
                break;
            }
        }
        invocation.setArguments(args);
    }

    /**
     * Callback that is invoked by {@link #readRemoteInvocation} when it
     * encounters an invalid request message.
     * <p>
     * The default implementation either discards the invalid message or throws
     * a MessageFormatException - according to the "ignoreInvalidRequests" flag,
     * which is set to "true" (that is, discard invalid messages) by default.
     * 
     * @param requestMessage
     *            the invalid request message
     * @return the RemoteInvocation to expose for the invalid request (typically
     *         <code>null</code> in case of an invalid message that will simply
     *         be ignored)
     * @throws javax.jms.JMSException
     *             in case of the invalid request supposed to lead to an
     *             exception (instead of ignoring it)
     * @see #readRemoteInvocation
     * @see #setIgnoreInvalidRequests
     */
    protected RemoteInvocation onInvalidRequest(Message requestMessage) throws JMSException {
        if (this.ignoreInvalidRequests) {
            logger.warn("Invalid request message will be discarded: " + requestMessage);
            return null;
        } else {
            throw new MessageFormatException("Invalid request message: " + requestMessage);
        }
    }

    @SuppressWarnings("unchecked")
	private MdpMethodConfig getMethodConfig(RemoteInvocation invocation) {
        String methodKey = invocation.getMethodName() + ClassUtils.classNamesToString(invocation.getParameterTypes());
        Method method = methodCache.get(methodKey);
        if (method == null) {
            try {
                method = getServiceInterface().getMethod(invocation.getMethodName(), invocation.getParameterTypes());
                methodCache.put(methodKey, method);
            } catch (Exception e) {
                logger.error(MdpMessages.getMessage("01020004", invocation), e);
            }
        }
        if (method != null) {
            return mdpServiceConfig.get(method);
        } else {
            return null;
        }
    }

    @Override
    protected Object getProxyForService() {
        checkService();
        checkServiceInterface();
        ProxyFactory proxyFactory = new ProxyFactory();
        // proxyFactory.setProxyTargetClass(true);// CGLIB proxy
        proxyFactory.addInterface(getServiceInterface());
        // if (isRegisterTraceInterceptor()) {
        proxyFactory.addAdvice(new RemoteInvocationTraceInterceptor(getExporterName()));
        // }
        proxyFactory.setTarget(getService());

        // 此处可放advisor
        // proxyFactory.addAdvice(MdpStatisticInterceptor.INSTANCE);
        return proxyFactory.getProxy(getBeanClassLoader());
    }

    public String getServerName() {
        return serverName;
    }

    public void setServerName(String serverName) {
        this.serverName = serverName;
    }
}
